فارسی

راهنمای جامع جنریک‌ها در تایپ‌اسکریپت، شامل سینتکس، مزایا، کاربردهای پیشرفته و بهترین شیوه‌ها برای مدیریت انواع داده‌های پیچیده در توسعه نرم‌افزار جهانی.

جنریک‌های تایپ‌اسکریپت: تسلط بر انواع داده‌های پیچیده برای برنامه‌های کاربردی قدرتمند

تایپ‌اسکریپت، که یک فوق مجموعه از جاوااسکریپت است، توسعه‌دهندگان را قادر می‌سازد تا با استفاده از تایپ‌دهی استاتیک، کدهای قوی‌تر و قابل نگهداری‌تری بنویسند. در میان قدرتمندترین ویژگی‌های آن، جنریک‌ها (generics) قرار دارند که به شما اجازه می‌دهند کدی بنویسید که بتواند با انواع داده‌های مختلف کار کند و در عین حال ایمنی نوع (type safety) را حفظ نماید. این راهنما یک بررسی جامع از جنریک‌های تایپ‌اسکریپت را ارائه می‌دهد، با تمرکز بر کاربرد آن‌ها در انواع داده‌های پیچیده در زمینه توسعه نرم‌افزار جهانی.

جنریک‌ها چه هستند؟

جنریک‌ها راهی برای نوشتن کدهای قابل استفاده مجدد فراهم می‌کنند که می‌توانند با انواع مختلف کار کنند. به جای نوشتن توابع یا کلاس‌های جداگانه برای هر نوعی که می‌خواهید پشتیبانی کنید، می‌توانید یک تابع یا کلاس واحد بنویسید که از پارامترهای نوع استفاده می‌کند. این پارامترهای نوع، جایگزین‌هایی برای انواع واقعی هستند که هنگام فراخوانی یا نمونه‌سازی تابع یا کلاس استفاده خواهند شد. این ویژگی به خصوص هنگام کار با ساختارهای داده‌ای پیچیده که نوع داده درون آن‌ها ممکن است متغیر باشد، بسیار مفید است.

مزایای استفاده از جنریک‌ها

سینتکس پایه‌ای جنریک‌ها

سینتکس پایه‌ای جنریک‌ها شامل استفاده از براکت‌های زاویه‌ای (< >) برای تعریف پارامترهای نوع است. این پارامترهای نوع معمولاً با نام‌های T، K، V و غیره نامگذاری می‌شوند، اما شما می‌توانید از هر شناسه معتبری استفاده کنید. در اینجا یک مثال ساده از یک تابع جنریک آورده شده است:


function identity<T>(arg: T): T {
  return arg;
}

let myString: string = identity<string>("hello");
let myNumber: number = identity<number>(123);
let myBoolean: boolean = identity<boolean>(true);

console.log(myString); // Output: hello
console.log(myNumber); // Output: 123
console.log(myBoolean); // Output: true

در این مثال، <T> یک پارامتر نوع به نام T تعریف می‌کند. تابع identity یک آرگومان از نوع T می‌گیرد و یک مقدار از نوع T برمی‌گرداند. هنگام فراخوانی تابع، می‌توانید پارامتر نوع را به صراحت مشخص کنید (مثلاً identity<string>) یا اجازه دهید تایپ‌اسکریپت آن را بر اساس نوع آرگومان استنتاج کند.

کار با انواع داده‌های پیچیده

جنریک‌ها زمانی ارزش ویژه‌ای پیدا می‌کنند که با انواع داده‌های پیچیده مانند آرایه‌ها، اشیاء و اینترفیس‌ها سر و کار داریم. بیایید برخی از سناریوهای رایج را بررسی کنیم:

آرایه‌های جنریک

شما می‌توانید از جنریک‌ها برای ایجاد توابع یا کلاس‌هایی استفاده کنید که با آرایه‌هایی از انواع مختلف کار می‌کنند:


function arrayToString<T>(arr: T[]): string {
  return arr.join(", ");
}

let numberArray: number[] = [1, 2, 3, 4, 5];
let stringArray: string[] = ["apple", "banana", "cherry"];

console.log(arrayToString(numberArray)); // Output: 1, 2, 3, 4, 5
console.log(arrayToString(stringArray)); // Output: apple, banana, cherry

در اینجا، تابع arrayToString یک آرایه از نوع T[] می‌گیرد و یک نمایش رشته‌ای از آرایه را برمی‌گرداند. این تابع با آرایه‌هایی از هر نوعی کار می‌کند و این باعث می‌شود که قابلیت استفاده مجدد بالایی داشته باشد.

اشیاء جنریک

جنریک‌ها همچنین می‌توانند برای تعریف توابع یا کلاس‌هایی که با اشیاء با ساختارهای مختلف کار می‌کنند، استفاده شوند:


interface Person {
  name: string;
  age: number;
  country: string; // برای زمینه جهانی، کشور اضافه شد
}

interface Product {
  id: number;
  name: string;
  price: number;
  currency: string; // برای زمینه جهانی، واحد پول اضافه شد
}

function displayInfo<T extends { name: string }>(item: T): void {
  console.log(`Name: ${item.name}`);
}

let person: Person = { name: "Alice", age: 30, country: "USA" };
let product: Product = { id: 1, name: "Laptop", price: 1200, currency: "USD" };

displayInfo(person); // Output: Name: Alice
displayInfo(product); // Output: Name: Laptop

در این مثال، تابع displayInfo یک شیء از نوع T می‌گیرد که باید یک ویژگی name از نوع رشته داشته باشد. عبارت extends { name: string } یک محدودیت (constraint) است که حداقل الزامات برای پارامتر نوع T را مشخص می‌کند. این تضمین می‌کند که تابع می‌تواند با خیال راحت به ویژگی name دسترسی پیدا کند.

کاربرد پیشرفته جنریک‌ها

جنریک‌های تایپ‌اسکریپت ویژگی‌های پیشرفته‌تری را ارائه می‌دهند که به شما امکان می‌دهند کدهای انعطاف‌پذیرتر و قدرتمندتری ایجاد کنید. بیایید برخی از این ویژگی‌ها را بررسی کنیم:

پارامترهای نوع چندگانه

شما می‌توانید توابع یا کلاس‌هایی با چندین پارامتر نوع تعریف کنید:


function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

interface Name {
  firstName: string;
}

interface Age {
  age: number;
}

const person: Name = { firstName: "Bob" };
const details: Age = { age: 42 };

const merged = merge(person, details);
console.log(merged.firstName); // Output: Bob
console.log(merged.age); // Output: 42

تابع merge دو شیء از انواع T و U می‌گیرد و یک شیء جدید برمی‌گرداند که شامل ویژگی‌های هر دو شیء است. این یک راه قدرتمند برای ترکیب داده‌ها از منابع مختلف است.

محدودیت‌های جنریک

همانطور که قبلاً نشان داده شد، محدودیت‌ها به شما اجازه می‌دهند انواعی را که می‌توانند با یک پارامتر نوع جنریک استفاده شوند، محدود کنید. این تضمین می‌کند که کد جنریک می‌تواند با خیال راحت روی انواع مشخص شده عمل کند.


interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

loggingIdentity([1, 2, 3]); // Output: 3
loggingIdentity("hello"); // Output: 5
// loggingIdentity(123); // Error: Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.

تابع loggingIdentity یک آرگومان از نوع T می‌گیرد که باید یک ویژگی length از نوع عدد داشته باشد. این تضمین می‌کند که تابع می‌تواند با خیال راحت به ویژگی length دسترسی پیدا کند.

کلاس‌های جنریک

جنریک‌ها همچنین می‌توانند با کلاس‌ها استفاده شوند:


class DataStorage<T> {
  private data: T[] = [];

  addItem(item: T) {
    this.data.push(item);
  }

  removeItem(item: T) {
    this.data = this.data.filter(d => d !== item);
  }

  getItems(): T[] {
    return [...this.data];
  }
}

const textStorage = new DataStorage<string>();
textStorage.addItem("apple");
textStorage.addItem("banana");
textStorage.removeItem("apple");
console.log(textStorage.getItems()); // Output: [ 'banana' ]

const numberStorage = new DataStorage<number>();
numberStorage.addItem(1);
numberStorage.addItem(2);
numberStorage.removeItem(1);
console.log(numberStorage.getItems()); // Output: [ 2 ]

کلاس DataStorage می‌تواند داده‌هایی از هر نوع T را ذخیره کند. این به شما امکان می‌دهد ساختارهای داده‌ای قابل استفاده مجدد ایجاد کنید که از نظر نوع ایمن هستند.

اینترفیس‌های جنریک

اینترفیس‌های جنریک برای تعریف قراردادهایی که می‌توانند با انواع مختلف کار کنند، مفید هستند. برای مثال:


interface Result<T, E> {
  success: boolean;
  data?: T;
  error?: E;
}

interface User {
  id: number;
  username: string;
  email: string;
}

interface ErrorMessage {
  code: number;
  message: string;
}

function fetchUser(id: number): Result<User, ErrorMessage> {
  if (id === 1) {
    return { success: true, data: { id: 1, username: "john.doe", email: "john.doe@example.com" } };
  } else {
    return { success: false, error: { code: 404, message: "User not found" } };
  }
}

const userResult = fetchUser(1);
if (userResult.success) {
  console.log(userResult.data.username);
} else {
  console.log(userResult.error.message);
}

اینترفیس Result یک ساختار جنریک برای نمایش نتیجه یک عملیات تعریف می‌کند. این می‌تواند یا داده‌هایی از نوع T یا خطایی از نوع E را شامل شود. این یک الگوی رایج برای مدیریت عملیات‌های ناهمزمان یا عملیات‌هایی است که ممکن است با شکست مواجه شوند.

انواع کمکی (Utility Types) و جنریک‌ها

تایپ‌اسکریپت چندین نوع کمکی داخلی ارائه می‌دهد که به خوبی با جنریک‌ها کار می‌کنند. این انواع کمکی می‌توانند به شما در تبدیل و دستکاری انواع به روش‌های قدرتمند کمک کنند.

Partial<T>

Partial<T> تمام ویژگی‌های نوع T را اختیاری می‌کند:


interface Person {
  name: string;
  age: number;
}

type PartialPerson = Partial<Person>;

const partialPerson: PartialPerson = { name: "Alice" }; // معتبر است

Readonly<T>

Readonly<T> تمام ویژگی‌های نوع T را فقط-خواندنی می‌کند:


interface Person {
  name: string;
  age: number;
}

type ReadonlyPerson = Readonly<Person>;

const readonlyPerson: ReadonlyPerson = { name: "Bob", age: 42 };
// readonlyPerson.age = 43; // خطا: نمی‌توان به 'age' مقدار داد زیرا یک ویژگی فقط-خواندنی است.

Pick<T, K>

Pick<T, K> مجموعه‌ای از ویژگی‌های K را از نوع T انتخاب می‌کند:


interface Person {
  name: string;
  age: number;
  email: string;
}

type NameAndAge = Pick<Person, "name" | "age">;

const nameAndAge: NameAndAge = { name: "Charlie", age: 28 };

Omit<T, K>

Omit<T, K> مجموعه‌ای از ویژگی‌های K را از نوع T حذف می‌کند:


interface Person {
  name: string;
  age: number;
  email: string;
}

type PersonWithoutEmail = Omit<Person, "email">;

const personWithoutEmail: PersonWithoutEmail = { name: "David", age: 35 };

Record<K, T>

Record<K, T> یک نوع با کلیدهای K و مقادیری از نوع T ایجاد می‌کند:


type CountryCodes = "US" | "CA" | "UK" | "DE" | "FR" | "JP" | "CN" | "IN" | "BR" | "AU"; // لیست گسترش‌یافته برای زمینه جهانی
type Currency = "USD" | "CAD" | "GBP" | "EUR" | "JPY" | "CNY" | "INR" | "BRL" | "AUD"; // لیست گسترش‌یافته برای زمینه جهانی

type CurrencyMap = Record<CountryCodes, Currency>;

const currencyMap: CurrencyMap = {
  "US": "USD",
  "CA": "CAD",
  "UK": "GBP",
  "DE": "EUR",
  "FR": "EUR",
  "JP": "JPY",
  "CN": "CNY",
  "IN": "INR",
  "BR": "BRL",
  "AU": "AUD",
};

انواع نگاشت‌شده (Mapped Types)

انواع نگاشت‌شده به شما اجازه می‌دهند تا با پیمایش ویژگی‌های انواع موجود، آن‌ها را تبدیل کنید. این یک راه قدرتمند برای ایجاد انواع جدید بر اساس انواع موجود است. برای مثال، شما می‌توانید یک نوع ایجاد کنید که تمام ویژگی‌های یک نوع دیگر را فقط-خواندنی کند:


interface Person {
  name: string;
  age: number;
}

type ReadonlyPerson = {
  readonly [K in keyof Person]: Person[K];
};

const readonlyPerson: ReadonlyPerson = { name: "Eve", age: 25 };
// readonlyPerson.age = 26; // خطا: نمی‌توان به 'age' مقدار داد زیرا یک ویژگی فقط-خواندنی است.

در این مثال، [K in keyof Person] تمام کلیدهای اینترفیس Person را پیمایش می‌کند، و Person[K] به نوع هر ویژگی دسترسی پیدا می‌کند. کلمه کلیدی readonly هر ویژگی را فقط-خواندنی می‌کند.

انواع شرطی (Conditional Types)

انواع شرطی به شما امکان می‌دهند تا انواعی را بر اساس شرایط تعریف کنید. این یک راه قدرتمند برای ایجاد انواعی است که با سناریوهای مختلف سازگار می‌شوند.


type NonNullable<T> = T extends null | undefined ? never : T;

type MaybeString = string | null | undefined;
type StringType = NonNullable<MaybeString>; // string

function getValue<T>(value: T): NonNullable<T> {
  if (value == null) { // هم null و هم undefined را بررسی می‌کند
    throw new Error("مقدار نمی‌تواند null یا undefined باشد");
  }
  return value as NonNullable<T>;
}

try {
  const validValue = getValue("hello");
  console.log(validValue.toUpperCase()); // خروجی: HELLO

  const invalidValue = getValue(null); // این یک خطا پرتاب می‌کند
  console.log(invalidValue); // این خط اجرا نخواهد شد
} catch (error: any) {
  console.error(error.message); // خروجی: مقدار نمی‌تواند null یا undefined باشد
}

در این مثال، نوع NonNullable<T> بررسی می‌کند که آیا T برابر با null یا undefined است. اگر باشد، never را برمی‌گرداند که به این معنی است که این نوع مجاز نیست. در غیر این صورت، T را برمی‌گرداند. این به شما امکان می‌دهد انواعی ایجاد کنید که تضمین شده غیر-تهی هستند.

بهترین شیوه‌ها برای استفاده از جنریک‌ها

در اینجا برخی از بهترین شیوه‌ها برای استفاده از جنریک‌ها آورده شده است:

مثال‌ها در یک زمینه جهانی

بیایید چند مثال از نحوه استفاده از جنریک‌ها در یک زمینه جهانی را در نظر بگیریم:

تبدیل ارز


interface ConversionRate {
  rate: number;
  fromCurrency: string;
  toCurrency: string;
}

function convertCurrency<T extends ConversionRate>(amount: number, rate: T): number {
  return amount * rate.rate;
}

const usdToEurRate: ConversionRate = { rate: 0.85, fromCurrency: "USD", toCurrency: "EUR" };
const amountInUSD = 100;
const amountInEUR = convertCurrency(amountInUSD, usdToEurRate);
console.log(`${amountInUSD} USD برابر است با ${amountInEUR} EUR`); // خروجی: 100 USD برابر است با 85 EUR

قالب‌بندی تاریخ


interface DateFormatOptions {
  locale: string;
  options: Intl.DateTimeFormatOptions;
}

function formatDate<T extends DateFormatOptions>(date: Date, format: T): string {
  return date.toLocaleDateString(format.locale, format.options);
}

const currentDate = new Date();

const usDateFormat: DateFormatOptions = { locale: "en-US", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const germanDateFormat: DateFormatOptions = { locale: "de-DE", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const japaneseDateFormat: DateFormatOptions = { locale: "ja-JP", options: { year: 'numeric', month: 'long', day: 'numeric' } };

console.log("تاریخ آمریکا: " + formatDate(currentDate, usDateFormat));
console.log("تاریخ آلمان: " + formatDate(currentDate, germanDateFormat));
console.log("تاریخ ژاپن: " + formatDate(currentDate, japaneseDateFormat));

سرویس ترجمه


interface Translation {
  [key: string]: string; // اجازه کلیدهای زبان پویا را می‌دهد
}

interface LanguageData<T extends Translation> {
  languageCode: string;
  translations: T;
}

const englishTranslations: Translation = {
  "hello": "Hello",
  "goodbye": "Goodbye",
  "welcome": "Welcome to our website!"
};

const spanishTranslations: Translation = {
  "hello": "Hola",
  "goodbye": "Adiós",
  "welcome": "¡Bienvenido a nuestro sitio web!"
};

const frenchTranslations: Translation = {
  "hello": "Bonjour",
  "goodbye": "Au revoir",
  "welcome": "Bienvenue sur notre site web !"
};


const languageData: LanguageData<typeof englishTranslations>[] = [
  {languageCode: "en", translations: englishTranslations },
  {languageCode: "es", translations: spanishTranslations },
  {languageCode: "fr", translations: frenchTranslations}
];

function translate<T extends Translation>(key: string, languageCode: string, languageData: LanguageData<T>[]): string {
  const lang = languageData.find(lang => lang.languageCode === languageCode);
  if (!lang) {
    return `ترجمه برای ${key} در زبان ${languageCode} یافت نشد.`;
  }
  return lang.translations[key] || `ترجمه برای ${key} یافت نشد.`;
}

console.log(translate("hello", "en", languageData)); // خروجی: Hello
console.log(translate("hello", "es", languageData)); // خروجی: Hola
console.log(translate("welcome", "fr", languageData)); // خروجی: Bienvenue sur notre site web !
console.log(translate("missingKey", "de", languageData)); // خروجی: ترجمه برای missingKey در زبان de یافت نشد.

نتیجه‌گیری

جنریک‌های تایپ‌اسکریپت ابزاری قدرتمند برای نوشتن کدهای قابل استفاده مجدد و ایمن از نظر نوع هستند که می‌توانند با انواع داده‌های پیچیده کار کنند. با درک سینتکس پایه، ویژگی‌های پیشرفته و بهترین شیوه‌های استفاده از جنریک‌ها، می‌توانید کیفیت و قابلیت نگهداری برنامه‌های تایپ‌اسکریپت خود را به طور قابل توجهی بهبود بخشید. هنگام توسعه برنامه‌ها برای مخاطبان جهانی، جنریک‌ها می‌توانند به شما در مدیریت فرمت‌های داده متنوع و قراردادهای فرهنگی کمک کنند و تجربه‌ای یکپارچه برای همه کاربران تضمین نمایند.